文章目录

翻译自Ditch the [].forEach.call(NodeList) hack

在我们进一步之前,我不得不承认,我曾经使用过这种技术。 这让它看起来很酷,很酷,我像是在做ECMA5 hack,但过了一段时间,我写出了更好的JavaScript,才发现这种技术只会引起头痛且是不必要的时髦代码,接下来我将把我的想法放在为什么我现在不主张这种技术。

理解 [].forEach.call(NodeList)

让我们来确定一下这个hack的实际情况,然后才能看出为什么我认为这是一个非常糟糕的技术。让我们创建一个普通的数组,并使用ECMA5的.forEach这个神奇的方法去遍历它:

1
2
3
4
var myArray = [1,2,3,4];
myArray.forEach(function (item) {
console.log(item); // prints each number
});

上面看起来不错,但”hack”在哪里适用?试试输入NodeList:

1
2
3
4
5
6
var myNodeList = document.querySelectorAll('li'); // 获取一些 <li>
// Uncaught TypeError: Object #<NodeList> has no method 'forEach'
myNodeList.forEach(function (item) {
// :(
});

这里我们遇到了一个错误,因为NodeLists不共享其中包含forEach方法的Array的原型,。呃,但是还是有”解决方案”:

1
NodeList.prototype.forEach = Array.prototype.forEach;

如果你这样做过,那可能不是一个好主意(请不要使用它)。通过原型扩展现有的DOM功能通常被认为是不好的做法,因为这可能会导致大量的问题。

解决这个问题更好的方法是:

1
2
3
4
var myNodeList = document.querySelectorAll('li'); // grabs some <li>
[].forEach.call(myNodeList, function (item) {
// :) hooray `item` can be used here
});

通过创建(空)数组并通过NodeList间接调用数组的原型的forEach方法,现在一切都奏效了。

现在来看看这个技术的一些问题

Problems

Problem #1: 没有数组方法

这是一个大问题。 NodeLists是有一个length属性的,如果要添加一个新元素或者从该列表中删除一个元素,你不能通过使用forEach的hack来保持任何状态,并且也无法访问列表本身,这意味着它是一条单向的方法,也就是说在静态元素的情况下你只能操纵一次,你不能继续添加/删除其他元素。

使用诸如.splice()之类的方法将导致错误 - 原因是NodeLists在它们的Prototype中不包含这个方法。节点列表不能被改变,这通常是非常不切实际的。这也意味着你除了可以绑定事件处理程序或调用方法外,不能做任何其它令人兴奋的操作。

Problem #2: 限制复用

当我们缓存一个dom时,我们缓存的不是一个数组,这意味着我们无法重复使用该方法。我认为这是一个严重的可扩展性和可重用性的问题。如果我想再次调用该方法怎么办?那我必须编写相同的非描述性代码两次。

1
2
3
4
5
6
7
8
// cached, we can access this again
var myNodeList = document.querySelectorAll('li');
// this will only get called once
// and cannot be called again
[].forEach.call(myNodeList, function (item) {
// :(
});

Problem #3: 分离问题

节点列表和数组是两个不同的数据类型,那么为什么我们编写这些重叠而不会为我们带来任何好处的代码呢?如果您需要一个NodeList中的数组,那么可以执行下面这种操作。这是非跨浏览器版本的方法:

1
var myArrayFromNodeList = [].slice.call(document.querySelectorAll('li'));

但是这又是一个Array.prototype hack,我也不鼓励使用这种方式。它并不能兼容所有的浏览器,因为IE不允许NodeLists形成Array.prototype.slice调用的宿主对象。不过,可以使用下面这种方法将所有节点推送到新数组中:

1
2
3
4
5
var myNodeList = document.querySelectorAll('li');
var myArrayFromNodeList = []; // empty at first
for (var i = 0; i < myNodeList.length; i++) {
myArrayFromNodeList.push(myNodeList[i]); // ahhh, push it
}

我们将使用我们的节点填充为数组! B-E-A-uuutiful。这给我们带来了什么好处?我们完全分离了两种对象类型,并且可以在需要时引用它们:

1
2
console.log(myNodeList); // NodeList
console.log(myArrayFromNodeList); // Array of Nodes

现在我们可以遍历我们的数组,并可以调用splice和push方法来实际做一些有价值的事情

Problem #4: 创建不必要的数组

使用[] .forEach.call实际上时创建了一个新的数组,然后它会驻留在内存中,我们显然没有必要这样做。然而有一个解决方法,那就是使用Array.prototype.forEach.call,这实际上更快也更可靠(一些库将会与使用[]语法的方式产生冲突),这样只是访问forEach方法,而不是创建一个新的数组然后访问它。

Problem #5: It’s slower and works harder

我不打算进行大规模辩论关于从方法中消耗的0.00012230毫秒,但[] .forEach.call真的非常慢,特别是因为它通常是通过实例化新对象(或类似的东西)的方式来生成新的元素。首先,[]实例化一个新的Array对象,然后forEach方法链接到.call()方法,最后它会改变每次循环的执行上下文。我不知道你,但是对于这样一个简单的任务,这样做了很多不必要的工作。

Problem #6: Stupidity vulnerabilities

根据先前我们看到的例子,你应该知道这个例子还是有效的

1
2
3
4
5
var myNodeList = document.querySelectorAll('li');
[1,2,3,4,5].forEach.call(myNodeList, function (item) {
// Wah?...
// Are we looping over the NodeList or Array?!
});

我不希望我的代码容易受到这种事情的影响,这也是可能会发生的事情

Problem #7: 可扩展性

如果我想将NodeList传递到另一种方法中,那我就必须完全重写forEach的hack,然后才能将其传递到一个方法中,这样就导致需要进行更多的测试和产生更多的bug的可能性。如果第一次能正确地编写好代码,那这将能够给代码带来极好的扩展性。

Problem #8: 可读性

一个随机的forEach(通常在脚本结尾看到)是完全没有意义的。循环通常基于操纵某种类型的对象/元素,因此将其包含在您自己的方法中可能会更好

Problem #9: Confusing syntax

你是在操作NodeList还是Array?当你可以轻松地编写一个方法来描述这些事情的时候为什么还让别人要弄清楚你是在操作哪个对象

Problem #10: 非跨浏览器兼容

我通常不会使用ECMAScript 5 forEach方法,一个简单的for循环就足够了:

1
2
3
4
var myNodeList = document.querySelectorAll('li');
for (var i = 0; i < myNodeList.length; i++) {
// do something with myNodeList[i]
}

先不说这样更快,这也让我有更多的方式去控制数组元素,例如,我可以反向循环(通常比正向循环更快!):

1
2
3
4
var myNodeList = document.querySelectorAll('li');
for (var i = myNodeList.length; i--;) { // reverse
// do something with myNodeList[i]
}

当然你也可以写一个函数去封装这些代码,这样你就可以在所有的浏览器中调用这个方法,因此你也可以少敲一些代码

Problem #11: 开发人员的误解

我看到过开发人员用这种方法来遍历数组,这样做是很愚蠢的,因为这个hack是针对NodeLists而不是数组的。

使用hack会有更多的问题,但现在我只在主要的领域发表一些看法

建议

我避开了基于上述的方法,使用更好的代码对我来说更明智。编写自己的forEach方法是很容易的,它避免了对hack方法的需求,而它只需要一个NodeList或Array做为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// forEach method, could be shipped as part of an Object Literal/Module
var forEach = function (array, callback, scope) {
for (var i = 0; i < array.length; i++) {
callback.call(scope, i, array[i]); // passes back stuff we need
}
};
// Usage:
// optionally change the scope as final parameter too, like ECMA5
var myNodeList = document.querySelectorAll('li');
forEach(myNodeList, function (index, value) {
console.log(index, value); // passes index + value back!
});

通过变量缓存来跟踪数组和节点列表状态,虽然写了一些额外的几行代码,但却让你的代码改善十倍,这将会在往后写代码的过程中我们将获益更多。

Any thoughts appreciated! Happy coding!

文章目录